package org.melanesia.ref;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Reference monitor is a helper class to monitor references that are not being
 * properly released by framework's user. The use of this class is as follows:
 *
 * <pre>
 * {@code
 *
 *   ReferenceMonitor.observe(objectToObserve)
 *     .withResource(resourceOne)
 *     .withResource(resourceTwo)
 *     ...;
 * }
 * </pre>
 *
 * In the sample above, {@code objectToObserve} is any object being created by
 * the framework. The {@code resourceOne} and {@code resourceTwo} are resources,
 * that are created together with {@code objectToObserve} (and usually, but not
 * necessarily stored in private fields of {@code objectToObserve}) that require
 * to be released explicitely.
 *
 * This can be a statement, a result set, a connection, file handler, etc.
 *
 * Note, that usually, such resources should be released as soon as possible,
 * and leaving this to framework users is a possible leak. However in some cases
 * (and the good example is {@link org.melanesia.sql.Query#iterator(Class)} the
 * resource cannot be released immediately and it is up to the user, to release
 * it. Another example is a JBoss WARNING logged when you do not release
 * connections to the pool yourself.
 *
 * So, what this class do, is store a @{link ObjectsReference} (which is a
 * subclass of {@link java.lang.ref.PhantomReference}) to given object, and
 * register this reference with a @{link {@link ReferenceQueue}. When observed
 * object becomes "phantom reachable", the reference is queued and the
 * {@link #cleanup()} method can release resources (if they were not released
 * earlier by the user). The {@link #cleanup()} method retreives releasable
 * resources attached to the reference and calls
 * {@link ReleasableResource#release()} on them. It is the responsibility of the
 * programmer to implement this interface for each type of releasable resource.
 *
 * @author marcin.kielar
 *
 */
public final class ReferenceMonitor {

    /** Class logger. */
    private static Logger logger = Logger.getLogger(ReferenceMonitor.class.getName());

    /** Reference queue. */
    private static final ReferenceQueue<Object> QUEUE = new ReferenceQueue<Object>();

    /** List of currently observed references. */
    private static final ArrayList<PhantomReference<Object>> REFERENCES = new ArrayList<PhantomReference<Object>>();

    /** Private constructor. */
    private ReferenceMonitor() { }

    /**
     * Observes a reference of passed objects.
     *
     * @param object
     *            an object to observe
     * @return reference to that object, allowing one to attach resources to it
     */
    public static ObjectReference observe(final Object object) {
        ObjectReference ref = new ObjectReference(QUEUE, object, createTrace());
        REFERENCES.add(ref);

        return ref;
    }

    /**
     * Internal method for creating traces of creation of unreleased objects.
     *
     * @return a trace
     */
    private static Throwable createTrace() {
        Throwable t = new Throwable();
        StackTraceElement[] os = t.getStackTrace();

        StackTraceElement[] ns = new StackTraceElement[os.length - 2];
        System.arraycopy(os, 2, ns, 0, os.length - 2);

        t.setStackTrace(ns);
        return t;
    }

    /**
     * Looks up the reference queue to see if there are any released references
     * and if so tries to clean them up by releasing attached resources.
     */
    public static void cleanup() {
        ObjectReference ref;
        while ((ref = (ObjectReference) QUEUE.poll()) != null) {
            for (ReleasableResource<?> resource : ref.getResources()) {
                try {
                    if (resource != null && !resource.isReleased()) {
                        logger.log(Level.WARNING, "Cleaning resources for you: " + resource.get().getClass(), ref.getTrace());
                        resource.release();
                    }
                } catch (Throwable t) {
                    logger.log(Level.SEVERE, "Error cleaning resources for you.", t);
                }
            }

            REFERENCES.remove(ref);
        }
    }
}
